Feature/plugin bundle phase2 - #261
Open
liyangbing wants to merge 3 commits into
Open
Conversation
Introduce a new capability kind 'bundle' that packages skills (and in later phases: server tools, client UI, hooks) as a single deployable plugin unit. This is Phase 0 of the Plugin platform architecture. Changes: - canonical: add KindBundle constant, BundleSpec struct with inline skills, Validate/UnmarshalJSON branches, ErrInvalidBundle sentinel - render: all 4 targets (ClaudeCode/OpenCode/Codex/Pi) support KindBundle - agentdaemon: resolveBundleCapability extracts inline skills and injects them as append-mode system prompts - store: normalizeCapabilityType and validateImportSpecPreCommit accept 'bundle' - dev routes: POST .../capabilities/plugins/install endpoint for CLI - dev routes: isListedCapabilityType includes 'bundle' so bundles appear in workspace capability listings - CLI: 'parsar plugin add/list/remove' subcommands that read a local plugin directory manifest, embed skill content, and call the server API - frontend: add Plugin tab to capability type filter, CapabilityTypeBadge handles 'bundle' type - examples: customer-service-skin demo plugin (skill-only, changes Agent persona to professional customer service style) Verified end-to-end: install plugin via API -> bind to Agent -> new conversation shows customer-service persona in responses (system prompt injection confirmed in server logs).
Plugin bundles with a server_entry now get their tools exposed as an
MCP server that the daemon spawns on demand.
Architecture:
- server/plugin-host/: Node.js MCP server (JSON-RPC 2.0 stdio)
loads plugin server modules and exposes ctx.tools.define API
- resolveBundleCapability emits {command:'node', args:[...]} when
ServerEntry is non-empty and PARSAR_PLUGIN_HOST_PATH is configured
- CLI 'parsar plugin add' copies server files to ~/.parsar/plugins/
- CLI 'parsar plugin remove' cleans up on-disk files
New env var: PARSAR_PLUGIN_HOST_PATH (absolute path to plugin-host/index.js)
Example: examples/plugins/hotel-ops/ with check_room_status and
suggest_pricing tools, validated end-to-end.
Also fixes Phase 0 web typecheck errors (missing i18n key for bundle
type, MarketplaceTab type narrowing).
Introduces the client-side plugin system: plugins can register React
components to named slots in the web UI, replacing or extending any
section of the interface.
Architecture:
- Slot registry (plugin-slots.ts): single/list/chain slot types with
version-counting useSyncExternalStore integration
- SlotRenderer components: ToolCardSlot, ListSlot, SingleSlot with
PluginErrorBoundary isolation
- Plugin loader: fetches /api/v1/plugins/{name}/client.js, executes
via new Function(), idempotent load/unload on capability changes
- window.__PARSAR_PLUGIN_API__ exposes shared React + definePlugin API
- CLI auto-builds client TSX with esbuild during plugin add
- Go endpoint serves built client.js from plugins directory
Slot extension points placed in:
- App.tsx: workspace.main (full-page takeover)
- ConversationsPage: workspace.content, conversation.header.actions,
conversation.input.dock, conversation.tool-card
- AdminLayout: layout.header.actions, layout.nav.bottom
Example: hotel-ops plugin extended with full workspace UI (KPI cards,
room grid, events panel) + custom tool-result cards (RoomStatusCard,
PricingCard).
Also bumps Go 1.25.12 → 1.25.13 to fix 7 stdlib vulnerabilities
(GO-2026-6218 through GO-2026-5026).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Plugin Phase 2: Client UI. Plugins can now register React components to named "slots" in the web UI, replacing or extending any section — from custom tool-result cards to full-page workspace takeover. This lets FDEs build complete business interfaces (hotel dashboard, e-commerce workbench) on top of Parsar without forking the frontend.
How
Slot registry (plugin-slots.ts): A version-counting singleton with three slot modes (single/list/chain). Components subscribe via useSyncExternalStore using the version number as snapshot — avoids the referential-instability trap of returning arrays directly.
Plugin loading: usePluginClients queries the capabilities API for bundles, fetches /api/v1/plugins/{name}/client.js, and executes via new Function(). On capability unbind, unloadPlugin() removes the slot registrations and the UI falls back to defaults immediately (React Query invalidation, no polling).
Build pipeline: CLI plugin add detects manifest.client.entry, invokes node build-client.js which runs esbuild in IIFE mode with an externalize-react plugin. React is shared from the host via window.PARSAR_PLUGIN_API — plugins never bundle their own React.
Trade-offs:
new Function() over dynamic import() — works without ESM module-serving infrastructure; blocked by strict CSP but enterprise private deployments don't typically enforce it.
React shared via window global over import-maps — simpler, no browser version constraints, same approach DSH uses (window.ModuleLoader).
Slot points are added incrementally (3-5 lines each) rather than making everything a plugin — pragmatic for an existing product vs. DSH's "everything is a plugin from day one" approach.
Also bumps Go 1.25.12 → 1.25.13 to resolve 7 stdlib vulnerabilities flagged by govulncheck.
Verification
make check-go (all Go tests pass)
make check-web (tsc typecheck + design lint)
make check-cli (CLI + opencode-plugin typecheck)
make check-hygiene
Manual smoke: installed hotel-ops with client → full-screen workspace renders (KPI cards, room grid, events panel). Unbinding the capability removes the workspace and restores default UI without page refresh.
Notes for reviewers
react-dom shim returns the React namespace — plugins cannot use createPortal/flushSync yet. Acceptable for Phase 2; will need a separate ReactDOM bridge if a future plugin requires portals.
extractPresentationFromSteps parses __parsar_presentation client-side from tool step results. No server-side extraction needed because the daemon already forwards MCP content blocks through the event pipeline.
Two pre-existing test failures (TestTruncate in CLI, TestInstallPlugins_ConcurrentSamePluginNoTruncation in claudecode) are unrelated to this PR — they reproduce on the base branch with both Go versions.
The workspace.main slot in App.tsx replaces everything including navigation. If only the chat area should be replaced, plugins should register workspace.content instead. Both coexist — plugin authors choose the scope they need.